Skip to content

Require explicit package manager for project operations#7242

Open
dmerand wants to merge 1 commit intodonald/package-manager-root-knownfrom
donald/package-manager-project-operations
Open

Require explicit package manager for project operations#7242
dmerand wants to merge 1 commit intodonald/package-manager-root-knownfrom
donald/package-manager-project-operations

Conversation

@dmerand
Copy link
Copy Markdown
Contributor

@dmerand dmerand commented Apr 10, 2026

What

This follow-up continues the same package-manager stack as #7239 and #7241.

Add an explicit project operation boundary for package-manager use, and use it in dependency install and extension generation paths instead of reading project.packageManager directly.

Why

The stack goal is to make callers choose intent, not implementation.

#7241 gives callers that already know the project root a more explicit path. This PR applies the same idea to operation-owned callers: install and mutation code should require a usable project package manager instead of letting 'unknown' flow into dependency helpers.

How

Add requireProjectPackageManagerForOperations(project) in packages/app/src/cli/utilities/project-package-manager.ts.

Use it from:

  • installAppDependencies(...)
  • extension generation dependency install paths

When the app root has no package.json, these operations now fail early with a clearer error instead of passing ambiguous package-manager state deeper into install code.

@dmerand dmerand requested a review from a team as a code owner April 10, 2026 00:10
Copy link
Copy Markdown
Contributor Author

dmerand commented Apr 10, 2026

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@afurm
Copy link
Copy Markdown

afurm commented Apr 12, 2026

Good addition — enforcing an explicit boundary for operations that require a known package manager is the right pattern. A few things to consider:

1. The 'unknown' sentinel type safety

The test file changes use ProjectPackageManager | 'unknown' as the type, but does ProjectPackageManager (the actual runtime type) include 'unknown' as a valid variant? If packageManager is typed as PackageManager (without 'unknown'), there's a type mismatch — the test is lying about the type to accommodate the new behavior. Worth verifying the actual type definition in node-package-manager.ts.

2. Should requirePackageManagerForOperations cache the result?

Each call to requirePackageManagerForOperations re-validates and returns the package manager. If this is called multiple times in a hot path (e.g., during installAppDependencies and then extension generation), is there any cost to re-checking? A cached getter might be cleaner, or at least a comment noting it's idempotent.

3. The test case — what is "no package.json" in CI/test environments?

The new test removes package.json and expects an AbortError. But in containerized or generated test fixtures, is there any scenario where a project legitimately has no package.json but should still be operable? The error message says "Could not determine" — is that always the right failure mode, or should there be a way to fall back to a CLI-wide default package manager?

@dmerand dmerand force-pushed the donald/package-manager-root-known branch from bfabe0f to 821d5db Compare April 13, 2026 18:34
@dmerand dmerand force-pushed the donald/package-manager-project-operations branch 2 times, most recently from 0214c2e to 68a6662 Compare April 13, 2026 18:49
@dmerand dmerand force-pushed the donald/package-manager-root-known branch from 821d5db to 25e1f67 Compare April 13, 2026 18:49
Copy link
Copy Markdown
Contributor

@ryancbahan ryancbahan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like the control hould be inverted. Rather than the project itself having a method that throws when its property is missing, the caller should check project.packageManager when it needs to and ignore it when it doesn't. If you really need a dedicated helper, you could just have a plain function that takes the project and does the check/narrowing.

@dmerand dmerand force-pushed the donald/package-manager-project-operations branch from 68a6662 to 1e6ebeb Compare April 13, 2026 19:55
@dmerand dmerand force-pushed the donald/package-manager-project-operations branch from 1e6ebeb to 738df5d Compare April 13, 2026 20:05
@github-actions
Copy link
Copy Markdown
Contributor

Differences in type declarations

We detected differences in the type declarations generated by Typescript for this branch compared to the baseline ('main' branch). Please, review them to ensure they are backward-compatible. Here are some important things to keep in mind:

  • Some seemingly private modules might be re-exported through public modules.
  • If the branch is behind main you might see odd diffs, rebase main into this branch.

New type declarations

We found no new type declarations in this PR

Existing type declarations

packages/cli-kit/dist/public/node/node-package-manager.d.ts
@@ -27,6 +27,7 @@ export type DependencyType = 'dev' | 'prod' | 'peer';
  */
 export declare const packageManager: readonly ["yarn", "npm", "pnpm", "bun", "homebrew", "unknown"];
 export type PackageManager = (typeof packageManager)[number];
+export type ProjectPackageManager = Extract<PackageManager, 'yarn' | 'npm' | 'pnpm' | 'bun'>;
 /**
  * Returns an abort error that's thrown when the package manager can't be determined.
  * @returns An abort error.
@@ -68,6 +69,58 @@ export declare function packageManagerFromUserAgent(env?: NodeJS.ProcessEnv): Pa
  * @returns The dependency manager
  */
 export declare function getPackageManager(fromDirectory: string): Promise<PackageManager>;
+/**
+ * Resolves the package manager for a directory already known to be the project root.
+ *
+ * Use this when the caller already knows the root directory and wants to inspect that root
+ * directly rather than walking upward from a child directory.
+ *
+ * @param rootDirectory - The known project root directory.
+ * @returns The package manager detected from root markers, defaulting to npm <command>
+
+Usage:
+
+npm install        install all the dependencies in your project
+npm install <foo>  add the <foo> dependency to your project
+npm test           run this project's tests
+npm run <foo>      run the script named <foo>
+npm <command> -h   quick help on <command>
+npm -l             display usage info for all commands
+npm help <term>    search for help on <term>
+npm help npm       more involved overview
+
+All commands:
+
+    access, adduser, audit, bugs, cache, ci, completion,
+    config, dedupe, deprecate, diff, dist-tag, docs, doctor,
+    edit, exec, explain, explore, find-dupes, fund, get, help,
+    help-search, init, install, install-ci-test, install-test,
+    link, ll, login, logout, ls, org, outdated, owner, pack,
+    ping, pkg, prefix, profile, prune, publish, query, rebuild,
+    repo, restart, root, run-script, sbom, search, set,
+    shrinkwrap, star, stars, start, stop, team, test, token,
+    undeprecate, uninstall, unpublish, unstar, update, version,
+    view, whoami
+
+Specify configs in the ini-formatted file:
+    /home/runner/work/_temp/.npmrc
+or on the command line via: npm <command> --key=value
+
+More configuration info: npm help config
+Configuration fields: npm help 7 config
+
+npm@11.3.0 /opt/hostedtoolcache/node/24.1.0/x64/lib/node_modules/npm when no marker exists.
+ * @throws PackageJsonNotFoundError if the provided directory does not contain a package.json.
+ */
+export declare function getPackageManagerForProjectRoot(rootDirectory: string): Promise<ProjectPackageManager>;
+/**
+ * Builds the command and argv needed to execute a local binary using the package manager
+ * detected from the provided directory or its ancestors.
+ */
+export declare function packageManagerBinaryCommandForDirectory(fromDirectory: string, binary: string, ...binaryArgs: string[]): Promise<{
+    command: string;
+    args: string[];
+}>;
 interface InstallNPMDependenciesRecursivelyOptions {
     /**
      * The dependency manager to use to install the dependencies.

Copy link
Copy Markdown
Contributor Author

dmerand commented Apr 13, 2026

This feels like the control hould be inverted. Rather than the project itself having a method that throws when its property is missing, the caller should check project.packageManager when it needs to and ignore it when it doesn't. If you really need a dedicated helper, you could just have a plain function that takes the project and does the check/narrowing.

​That works for me. I took another pass, wanna give it a look?

@dmerand dmerand requested a review from ryancbahan April 13, 2026 20:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants